/*
 * Galaxium Messenger
 * 
 * Copyright (C) 2008 Paul Burton <paulburton89@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.Collections.Generic;

using Anculus.Core;

namespace Galaxium.Protocol.Msn
{
	public enum MsnP2PSessionState { Error, WaitingForLocal, WaitingForRemote, Active, Closing, Closed }

	public class MsnP2PSession : IDisposable
	{
		public event EventHandler Activated;
		public event EventHandler Closing;
		public event EventHandler Closed;
		public event EventHandler Error;
		public event EventHandler Waiting;
		
		IMsnP2PSessionApplication _app;
		IMsnP2PBridge _bridge;
		SLPRequestMessage _invite;
		MsnAccount _local;
		MsnContact _remote;
		MsnSession _session;
		MsnP2PSessionState _state = MsnP2PSessionState.Closed;
		uint _sessionID;
		uint _localID;
		uint _localBaseID;
		uint _remoteID;
		uint _remoteBaseID;
		
		Queue<P2PMessage> _outQueue = new Queue<P2PMessage> ();
		
		static Random _random = new Random ();

		public IMsnP2PApplication Application
		{
			get { return _app; }
		}
		
		public IMsnP2PBridge Bridge
		{
			get { return _bridge; }
		}
		
		public SLPRequestMessage Invite
		{
			get { return _invite; }
		}
		
		public MsnAccount Local
		{
			get { return _local; }
		}
		
		public MsnContact Remote
		{
			get { return _remote; }
		}
		
		public uint LocalID
		{
			get { return _localID; }
			set { _localID = value; }
		}
		
		public uint RemoteID
		{
			get { return _remoteID; }
		}
		
		public uint LocalBaseID
		{
			get { return _localBaseID; }
		}
		
		public uint RemoteBaseID
		{
			get { return _remoteBaseID; }
		}
		
		public MsnSession Session
		{
			get { return _session; }
		}
		
		public uint SessionID
		{
			get { return _sessionID; }
		}
		
		public MsnP2PSessionState State
		{
			get { return _state; }
		}

		public MsnP2PSession (IMsnP2PSessionApplication app)
		{
			_app = app;
			_local = app.Local;
			_remote = app.Remote;
			_session = _local.Session;
			_sessionID = (uint)_random.Next (10000, int.MaxValue);
			
			_localBaseID = (uint)_random.Next (10000, int.MaxValue);
			_localID = _localBaseID;
			
			_app.P2PSession = this;
			_app.Complete += AppComplete;
			
			Log.Debug ("P2PSession {0} created (Initiated locally)", _sessionID);
			
			_invite = new SLPRequestMessage (_remote, "INVITE");
			_invite.ContentType = "application/x-msnmsgr-sessionreqbody";
			_invite.MIMEBody["EUF-GUID"] = MsnP2PUtility.GetEufGuid (_app).ToString ("B").ToUpperInvariant ();
			_invite.MIMEBody["SessionID"] = _sessionID.ToString ();
			_invite.MIMEBody["AppID"] = _app.AppID.ToString ();
			_invite.MIMEBody["Context"] = _app.CreateInviteContext ();
			
			_state = MsnP2PSessionState.WaitingForRemote;
			Log.Debug ("P2PSession {0} state {1}", _sessionID, _state);
			
			OnWaiting ();
			
			Send (_invite, delegate (P2PMessage ack)
			{
				_remoteBaseID = ack.Header.MessageID;
				_remoteID = _remoteBaseID;
			});
		}
		
		public MsnP2PSession (P2PMessage invite)
		{
			_invite = invite.SLPMessage as SLPRequestMessage;
			
			_session = _invite.Session;
			_local = _invite.To as MsnAccount;
			_remote = _invite.From as MsnContact;
			
			_localBaseID = (uint)_random.Next (10000, int.MaxValue);
			_localID = _localBaseID;
			_remoteBaseID = invite.Header.MessageID;
			_remoteID = _remoteBaseID;
			
			if (!uint.TryParse (_invite.MIMEBody["SessionID"].Value, out _sessionID))
				Log.Warn ("Unable to parse invite SessionID");
			
			Log.Debug ("P2PSession {0} created (Initiated remotely)", _sessionID);
			
			// Send Base ID
			
			Send (invite.CreateAck ());
			
			// Initialize P2P Application
			
			Type appType = MsnP2PUtility.GetApp (new Guid (_invite.MIMEBody["EUF-GUID"].Value));
			
			if (appType != null)
				_app = Activator.CreateInstance (appType, this) as IMsnP2PSessionApplication;
			else
			{
				Log.Warn ("Unknown app for EUF-GUID {0}", _invite.MIMEBody["EUF-GUID"].Value);
				Log.Debug ("Invite:\n{0}", invite);
			}
			
			if (!_app.CheckInvite (_invite))
			{
				Log.Warn ("P2PSession {0} app rejects invite\n{1}", _sessionID, invite);
				
				OnError ();
				
				SLPStatusMessage slp = new SLPStatusMessage (_remote, 500, "Internal Server Error");
				slp.Branch = _invite.Branch;
				slp.CallID = _invite.CallID;
				slp.ContentType = "application/x-msnmsgr-sessionreqbody";
				slp.MIMEBody["SessionID"] = _sessionID.ToString ();
				
				Send (slp, delegate
				{
					Close ();
				});
				
				return;
			}
			
			_app.Complete += AppComplete;
			
			_state = MsnP2PSessionState.WaitingForLocal;
			Log.Debug ("P2PSession {0} state {1}", _sessionID, _state);
			
			if (_app.AutoAccept)
				Accept ();
			else
				OnWaiting ();
		}
		
		public void Dispose ()
		{
			DisposeApp ();
			RemoveBridgeHandlers ();
			_bridge = null;
		}
		
		void AppComplete (object sender, EventArgs args)
		{
			DisposeApp ();
		}
		
		public void Accept ()
		{
			if (_state != MsnP2PSessionState.WaitingForLocal)
			{
				Log.Warn ("Accept called, but we're not waiting for the local client (State {0})", _state);
				return;
			}
			
			Log.Debug ("P2PSession {0} accepted", _sessionID);
			
			SLPStatusMessage slp = new SLPStatusMessage (_remote, 200, "OK");
			slp.Branch = _invite.Branch;
			slp.CallID = _invite.CallID;
			slp.ContentType = "application/x-msnmsgr-sessionreqbody";
			slp.MIMEBody["SessionID"] = _sessionID.ToString ();
			
			Send (slp, delegate
			{
				OnActive ();
				
				if (_app != null)
					_app.Begin ();
				else
					Log.Warn ("Unable to begin p2p application, object is null!");
			});
		}
		
		public void Decline ()
		{
			if (_state != MsnP2PSessionState.WaitingForLocal)
			{
				Log.Warn ("Declined called, but we're not waiting for the local client");
				return;
			}
			
			Log.Debug ("P2PSession {0} declined", _sessionID);
			
			SLPStatusMessage slp = new SLPStatusMessage (_remote, 603, "Decline");
			slp.Branch = _invite.Branch;
			slp.CallID = _invite.CallID;
			slp.ContentType = "application/x-msnmsgr-sessionreqbody";
			slp.MIMEBody["SessionID"] = _sessionID.ToString ();
			
			Send (slp, delegate
			{
				Close ();
			});
		}
		
		public void Close ()
		{
			Log.Debug ("P2PSession {0} closing", _sessionID);
			
			OnClosing ();
			
			SLPMessage slp = new SLPRequestMessage (_remote, "BYE");
			slp.CallID = _invite.CallID;
			slp.ContentType = "application/x-msnmsgr-sessionclosebody";
			slp.MIMEBody["SessionID"] = _sessionID.ToString ();
			
			P2PMessage msg = new P2PMessage (_session);
			msg.SLPMessage = slp;
			
			Send (msg, delegate
			{
				Log.Debug ("P2PSession {0} closed", _sessionID);
				
				OnClosed ();
			});
		}
		
		public bool ProcessMessage (IMsnP2PBridge bridge, P2PMessage msg)
		{
			_remoteID = msg.Header.MessageID;
			
			if ((msg.Header.Flags & P2PHeaderFlag.Waiting) == P2PHeaderFlag.Waiting)
			{
				//TODO: what should we do with these?
				return true;
			}
			
			if ((_state == MsnP2PSessionState.Closed) || (_state == MsnP2PSessionState.Closing) || (_state == MsnP2PSessionState.Error))
			{
				Log.Warn ("P2PSession {0} received message whilst in '{1}' state", _sessionID, _state);
				return false;
			}
			
			if (msg.SLPMessage != null)
			{
				if (msg.SLPMessage is SLPRequestMessage)
				{
					SLPRequestMessage req = msg.SLPMessage as SLPRequestMessage;
					
					if ((req.ContentType == "application/x-msnmsgr-sessionclosebody") && (req.Method == "BYE"))
					{
						P2PMessage byeAck = msg.CreateAck ();
						byeAck.Header.Flags = P2PHeaderFlag.CloseSession;
						
						Send (byeAck);
						
						OnClosed ();
						
						return true;
					}
					else if (req.ContentType == "application/x-msnmsgr-transreqbody")
					{
						// Direct connection invite
						
						Send (msg.CreateAck ());
						
						return true;
					}
				}
				else if (msg.SLPMessage is SLPStatusMessage)
				{
					SLPStatusMessage status = msg.SLPMessage as SLPStatusMessage;
					
					Send (msg.CreateAck ());
					
					if (status.Code == 200) // OK
					{
						OnActive ();
						
						_app.Begin ();
						
						return true;
					}
					else if (status.Code == 603) // Decline
					{
						OnClosed ();
						
						return true;
					}
					else
					{
						OnError ();
						
						return true;
					}
				}
			}
			
			if (_app == null)
			{
				Log.Warn ("P2PSession {0}: Received message for P2P app, but it's either been disposed or not created", _sessionID);
				return false;
			}
			
			return _app.ProcessMessage (bridge, msg);
		}
		
		public void Send (P2PMessage msg, AckHandler ackHandler)
		{
			msg.Header.MessageID = NextID ();
			
			P2PMessage[] msgs = MsnP2PUtility.SplitMessage (msg, _bridge != null ? _bridge.MaxDataSize : 1150);
			
			lock (_outQueue)
			{
				foreach (P2PMessage m in msgs)
					_outQueue.Enqueue (m);
			}
			
			if (ackHandler != null)
				MsnP2PUtility.RegisterAckHandler (msg.Header.AckID, ackHandler);
			
			ProcessOutQueue (false);
		}
		
		public void Send (P2PMessage msg)
		{
			Send (msg, null);
		}
		
		public void Send (SLPMessage slp, AckHandler ackHandler)
		{
			P2PMessage msg = new P2PMessage (_session);
			msg.SLPMessage = slp;
			
			Send (msg, ackHandler);
		}
		
		public void Send (SLPMessage slp)
		{
			Send (slp, null);
		}
		
		void ProcessOutQueue (bool forceReady)
		{
			if (_outQueue.Count == 0)
			{
				//Log.Debug ("Out queue is empty");
				return;
			}
			
			if (_bridge == null)
			{
				//Log.Debug ("Calling GETSWITCHBOARD from the MsnP2PSession");
				
				_bridge = _session.GetSwitchboard (false, _remote);
				AddBridgeHandlers ();
			}
			
			if (!(_bridge.Ready || forceReady))
			{
				//Log.Debug ("Bridge not ready");
				return;
			}
			
			lock (_outQueue)
				_bridge.Send (_outQueue.Dequeue ());
		}
		
		void AddBridgeHandlers ()
		{
			_bridge.BridgeReady += BridgeReady;
			_bridge.BridgeClosed += BridgeClosed;
			(_bridge as SBConnection).ContactLeft += BridgeContactLeft;
		}
		
		void RemoveBridgeHandlers ()
		{
			if (_bridge == null)
				return;
			
			_bridge.BridgeReady -= BridgeReady;
			_bridge.BridgeClosed -= BridgeClosed;
			(_bridge as SBConnection).ContactLeft -= BridgeContactLeft;
		}
		
		void BridgeReady (object sender, EventArgs args)
		{
			//Log.Debug ("P2PSession {0} bridge ready, {1} messages queued", _sessionID, _outQueue.Count);
			ProcessOutQueue (true);
		}
		
		void BridgeClosed (object sender, EventArgs args)
		{
			RemoveBridgeHandlers ();
			
			_bridge = null;
			
			if (_outQueue.Count > 0)
				ProcessOutQueue (false);
		}
		
		void BridgeContactLeft (object sender, ContactEventArgs args)
		{
			RemoveBridgeHandlers ();
			
			_bridge = null;
			
			OnClosed ();
		}
		
		public uint NextID ()
		{
			if (_localID == _localBaseID)
				_localID++;
			
			return _localID++;
		}
		
		protected virtual void OnClosing ()
		{
			_state = MsnP2PSessionState.Closing;
			Log.Debug ("P2PSession {0} state {1}", _sessionID, _state);
			
			if (Closing != null)
				Closing (this, EventArgs.Empty);
			
			DisposeApp ();
		}
		
		protected virtual void OnClosed ()
		{
			_state = MsnP2PSessionState.Closed;
			Log.Debug ("P2PSession {0} state {1}", _sessionID, _state);
			
			if (Closed != null)
				Closed (this, EventArgs.Empty);
			
			DisposeApp ();
		}
		
		protected virtual void OnError ()
		{
			_state = MsnP2PSessionState.Error;
			Log.Debug ("P2PSession {0} state {1}", _sessionID, _state);
			
			if (Error != null)
				Error (this, EventArgs.Empty);
			
			DisposeApp ();
		}
		
		protected virtual void OnWaiting ()
		{
			if (Waiting != null)
				Waiting (this, EventArgs.Empty);
		}
		
		protected virtual void OnActive ()
		{
			_state = MsnP2PSessionState.Active;
			Log.Debug ("P2PSession {0} state {1}", _sessionID, _state);
			
			if (Activated != null)
				Activated (this, EventArgs.Empty);
		}
		
		void DisposeApp ()
		{
			if (_app != null)
			{
				_app.Dispose ();
				_app = null;
			}
		}
	}
}
